昨天我們實作了台鐵半自動台鐵訂票,雖然已經大幅減少我們人工流程的時間,但肯定不是完美,我們今天就來將驗證碼的部分也自動化。
先 demo 一下今天結果。
台鐵使用的驗證碼其實算是挺簡單的,是利用 session 來產生一張數字變形和干擾的圖片,然後讓使用者辨別數字,在將數字與 session 一起驗證。
而一般要解決 captcha,我們要不是去找出他所使用的加密演算法,要不就是尋找流程漏洞,而最後、最直接的辦法就是與他正面對決。一些夥伴熟悉 AI、圖形辨識的,可能會使用一些 opensource 的圖形辨識工具,然後再 training 出可以辨識的 model,不過小弟在那個領域外行,所以我就直接使用最傳統的做法,花錢解決XDD
這次的我們使用第三方的 service 對決驗證碼,我所使用的是 deathbycaptcha(以下簡稱 DBC),每次的費用大概是 NTD 0.05 元,算是挺經濟的,我自好幾年前買的 5000 個 request,到現在都還沒用完就是。
坊間還有許多其他類似的服務和工具,也歡迎有研究的夥伴一起留言討論。
我們可以先測試一下 DBC 能不能正常解決台鐵的驗證碼,可以直接到 DBC 的網站註冊帳號,然後應該可以 upload 圖片,我們將台鐵的驗證碼下載下來後上傳試試看,確認可以順利破解,不過破解的時間不算快,但還能接受就是。
接下來我們可以看一下 DBC 所提供的破解 api,他還有提供很許多不同語言的 library,但沒有 javascript,不過下面有提到可以自己 call api,所以最不濟我們就自幹。但是強大的 npm 裡面肯定是已經有社群夥伴撰寫,我們搜尋一下,確實是有的,然後就能夠進入程式碼實作了。
我們這次是改寫昨天的程式碼,昨天在處理驗證碼的步驟是:
那我們今天要把步驟改成
我們先來撰寫 solveCaptcha,主要就是 call dbc.solve 然後將結果傳給 callback。
function solveCaptcha() {
return new Promise(done => {
dbc.solve(fs.readFileSync(__dirname + '/code.jpeg'), function(err, id, solution) {
console.log('驗證碼 => '+solution);
done(solution)
});
})
}
我們先 mark 原本顯示圖片於 console 和接受使用者 input 的部分,然後將 solveCaptcha 加進去,這樣就改寫完成了。
initCookie()
.then(() => {
console.log('-> 取得驗證碼');
return fillInfo()
})
// .then(() => {
// consoleJpeg.attachTo(console)
// return console.jpeg(fs.readFileSync(__dirname + '/code.jpeg'))
// })
// .then(() => {
// return getCodeFromConsole()
// })
.then(() => {
console.log('-> 等待 DBC 破解驗證碼');
return solveCaptcha()
})
.then((code) => {
console.log('-> 執行訂票');
return takeOrder(code);
})
.then((orderNumber) => {
console.log('-> 訂位代號 => '+orderNumber);
})
const request = require('request').defaults({
jar: true,
headers: {
cookie: 'NSC_BQQMF=ffffffffaf121a1e45525d5f4f58455e445a4a423660',
}
})
const cheerio = require('cheerio');
const fs = require('fs');
const DeathByCaptcha = require("deathbycaptcha");
var dbc = new DeathByCaptcha("DBC 帳號", "DBC 密碼");
initCookie()
.then(() => {
console.log('-> 取得驗證碼');
return fillInfo()
})
// .then(() => {
// consoleJpeg.attachTo(console)
// return console.jpeg(fs.readFileSync(__dirname + '/code.jpeg'))
// })
// .then(() => {
// return getCodeFromConsole()
// })
.then(() => {
console.log('-> 等待 DBC 破解驗證碼');
return solveCaptcha()
})
.then((code) => {
console.log('-> 執行訂票');
return takeOrder(code);
})
.then((orderNumber) => {
console.log('-> 訂位代號 => '+orderNumber);
})
var reservationInfo = {
person_id: 'A134405743', // 這是 fake 身分證
from_station: '175',
to_station: '185',
getin_date: '2018/01/01-06',
train_no: '105',
order_qty_str: '1',
returnTicket: '0',
}
function initCookie() {
return new Promise(done => {
request('http://railway.hinet.net', (err, res, body) => {
done()
})
})
}
function fillInfo() {
return new Promise(done => {
var options = {
url: 'http://railway.hinet.net/check_ctno1.jsp',
method: 'POST',
form: reservationInfo,
headers: {
referer: 'http://railway.hinet.net/ctno1.htm',
}
}
request(options, (err, res, body) => {
var $ = cheerio.load(body)
$('body').append($('table noscript').html())
request('http://railway.hinet.net/' + $('#idRandomPic').attr('src')).pipe(fs.createWriteStream('code.jpeg')).on('close', done)
})
})
}
function solveCaptcha() {
return new Promise(done => {
dbc.solve(fs.readFileSync(__dirname + '/code.jpeg'), function(err, id, solution) {
console.log('驗證碼 => '+solution);
done(solution)
});
})
}
function takeOrder(code) {
return new Promise(done => {
reservationInfo.randInput = code
var options = {
url: 'http://railway.hinet.net/order_no1.jsp',
method: 'POST',
form: reservationInfo,
headers: {
referer: 'http://railway.hinet.net/check_ctno1.jsp',
}
}
request(options, (err, res, body) => {
var $ = cheerio.load(body)
done($('#spanOrderCode').text())
})
})
}
DBC 的破解率雖然很高,但並不是百分之百成功,所以我們應該要再加上失敗時候的回報機制,這樣才能避免跑自動流程失敗的時候一直在浪費資源。
另外目前 DBC 也能夠破解 reCAPTCHA,但我自己是還沒有嘗試過,也挖坑給各位去嘗試挑戰看看。